﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Red.Core.Network.PacketNetwork;

namespace Red.Core.Network
{
    public class RawTcpManager : IRawConnectionsManager
    {
        private class RawListener
        {
            public int AssignedID;
            public Socket AssignedSocket;
            public IRawListenerInterface AssignedCallback;
            public bool RemoveRequest = false;
        }

        private class RawConnection
        {
            public int AssignedID;
            public int AssignedListenerID = -1;
            public Socket AssignedSocket;
            public IRawConnectionInterface AssignedCallback;
            public bool RemoveRequest = false;

            public byte[] ReceiveBuffer = new byte[ 1024 * 2048 ];
        }

        private class SendingContext
        {
            public RawConnection Connection;
            public AsyncSendFinishedEventHandler FinishedEventHandler;

            public SendingContext( RawConnection connectionParam, AsyncSendFinishedEventHandler finishedEventHandlerParam )
            {
                Connection = connectionParam;
                FinishedEventHandler = finishedEventHandlerParam;
            }
        }

        private IDAllocator _idAllocator = new IDAllocator();
        private ConcurrentDictionary<int, RawListener> _listeners = new ConcurrentDictionary<int, RawListener>();
        private ConcurrentDictionary<int, RawConnection> _connections = new ConcurrentDictionary<int, RawConnection>();
        private Action<Exception> _exceptionThrown = null;

        public delegate void AsyncSendFinishedEventHandler( int connectionID );



        public RawTcpManager( Action<Exception> exceptionThrownHandler )
        {
            _exceptionThrown = exceptionThrownHandler;
        }

        public int CreateListener( CommAddress localAddress, IRawListenerInterface callback )
        {
            Socket socket = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );

            try
            {
                TcpAddress tcpAddr = localAddress as TcpAddress;
                socket.Bind( new IPEndPoint( IPAddress.Loopback, tcpAddr.EndPoint.Port ) );
                socket.Listen( 10 );
            }
            catch ( Exception ex )
            {
                Debug.Write( "Create listener exception: " + ex.Message );
                return 0;
            }

            int id = _idAllocator.AllocateID();

            RawListener listener = new RawListener();
            listener.AssignedID = id;
            listener.AssignedSocket = socket;
            listener.AssignedCallback = callback;

            try
            {
                listener.AssignedSocket.BeginAccept( new AsyncCallback( AcceptCallback ), listener );
            }
            catch ( Exception ex )
            {
                Debug.Write( "Create listener exception: " + ex.Message );
                return 0;
            }

            _listeners.AddOrUpdate( id, listener, ( key, oldValue ) => listener );

            return id;
        }

        public async Task<int> CreateConnection( CommAddress address, IRawConnectionInterface callback )
        {
            return await Task.Run( () =>
            {
                TcpAddress tcpAddr = address as TcpAddress;
                int id = _idAllocator.AllocateID();

                Socket newSocket = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );

                try
                {
                    newSocket.Connect( tcpAddr.EndPoint.Address, tcpAddr.EndPoint.Port );
                    newSocket.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true );
                }
                catch ( Exception ex )
                {
                    Debug.WriteLine( "Creating connection: " + ex.Message );
                    return 0;
                }

                newSocket.Blocking = false;

                RawConnection connection = new RawConnection();
                connection.AssignedID = id;
                connection.AssignedSocket = newSocket;
                connection.AssignedCallback = callback;

                _connections.AddOrUpdate( id, connection, ( key, oldValue ) => connection );

                connection.AssignedSocket.BeginReceive( connection.ReceiveBuffer, 0, connection.ReceiveBuffer.Count(), SocketFlags.None, new AsyncCallback( ReceiveCallback ), connection );

                return connection.AssignedID;
            });
        }

        public void CloseListener( int listenerID )
        {
            RawListener listener;
            bool listenerFound = _listeners.TryGetValue( listenerID, out listener );
            if ( listenerFound )
            {
                foreach ( KeyValuePair<int, RawConnection> pair in _connections )
                {
                    if ( pair.Value.AssignedListenerID == listenerID )
                    {
                        RawConnection removedConnection;
                        _connections.TryRemove( pair.Key, out removedConnection );

                        try
                        {
                            pair.Value.AssignedSocket.Shutdown( SocketShutdown.Both );
                        }
                        catch ( SocketException ex )
                        {
                            Debug.WriteLine( "CloseConnection: " + ex.Message );
                        }
                    }
                }

                try
                {
                    listener.AssignedSocket.Shutdown( SocketShutdown.Both );
                }
                catch ( SocketException ex )
                {
                    if ( ex.SocketErrorCode == SocketError.NotConnected )
                    {
                        Debug.WriteLine( "Connection was already shutdown" );
                    }
                    else
                    {
                        throw ex;
                    }
                }
            }
        }

        public void CloseConnection( int connectionID )
        {
            RawConnection connection;
            bool connectionFound = _connections.TryGetValue( connectionID, out connection );
            if ( connectionFound )
            {
                try
                {
                    connection.AssignedSocket.Shutdown( SocketShutdown.Both );
                }
                catch ( SocketException ex )
                {
                    Debug.WriteLine( "CloseConnection: " + ex.Message );
                }
            }
        }

        public void SendAsync( int connectionID, byte[] buffer )
        {
            SendAsync( connectionID, buffer, ( sendingConnectionID ) => { } );
        }

        public void SendAsync( int connectionID, byte[] buffer, AsyncSendFinishedEventHandler finishedEventHandler )
        {
            RawConnection connection;
            bool connectionFound = _connections.TryGetValue( connectionID, out connection );
            if ( connectionFound )
            {
                try
                {
                    connection.AssignedSocket.BeginSend( buffer, 0, buffer.Count(), SocketFlags.None, new AsyncCallback( SendCallback ), new SendingContext( connection, finishedEventHandler ) );
                }
                catch ( SocketException ex )
                {
                    if ( ex.SocketErrorCode == SocketError.Shutdown || ex.SocketErrorCode == SocketError.ConnectionAborted || ex.SocketErrorCode == SocketError.ConnectionReset )
                    {
                        Debug.WriteLine( "Connection {0} was closed.", connection.AssignedID );
                        connection.AssignedSocket.Close();
                        connection.AssignedCallback.OnDisconnected( connection.AssignedID );
                    }
                    else
                    {
                        OnExceptionThrown( ex );
                    }
                }
            }
        }

        public void SendCallback( IAsyncResult result )
        {
            SendingContext context = ( SendingContext )result.AsyncState;
            RawConnection connection = context.Connection;
            int bytesSent = connection.AssignedSocket.EndSend( result );
            context.FinishedEventHandler( connection.AssignedID );
        }

        protected void OnExceptionThrown( Exception ex )
        {
            _exceptionThrown?.Invoke( ex );
        }

        private void AcceptCallback( IAsyncResult result )
        {
            RawListener listener = ( RawListener )result.AsyncState;

            Socket socket = listener.AssignedSocket.EndAccept( result );
            listener.AssignedSocket.BeginAccept( new AsyncCallback( AcceptCallback ), listener );
            socket.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true );

            int connectionId = _idAllocator.AllocateID();
            IRawConnectionInterface connectionCallback;
            bool connectionAccpeted = listener.AssignedCallback.OnConnection( connectionId, out connectionCallback );

            if ( connectionCallback != null )
            {
                RawConnection rawConnection = new RawConnection();
                rawConnection.AssignedID = connectionId;
                rawConnection.AssignedListenerID = listener.AssignedID;
                rawConnection.AssignedSocket = socket;
                rawConnection.AssignedCallback = connectionCallback;

                _connections.AddOrUpdate( connectionId, rawConnection, ( key, oldValue ) => rawConnection );

                rawConnection.AssignedSocket.BeginReceive( rawConnection.ReceiveBuffer, 0, rawConnection.ReceiveBuffer.Count(), SocketFlags.None, new AsyncCallback( ReceiveCallback ), rawConnection );
            }
        }

        private void Disconnect( RawConnection connection )
        {
            connection.AssignedCallback.OnDisconnected( connection.AssignedID );
            connection.AssignedSocket.Close();
            Disconnected?.Invoke( connection.AssignedID );
        }

        private void ReceiveCallback( IAsyncResult result )
        {
            RawConnection connection = ( RawConnection )result.AsyncState;

            int bytesRead = 0;
            try
            {
                bytesRead = connection.AssignedSocket.EndReceive( result );
            }
            catch ( SocketException ex )
            {
                Debug.WriteLine( "ReadCallback: " + ex.Message );

                if ( ex.SocketErrorCode == SocketError.ConnectionReset )
                {
                    Disconnect( connection );
                }

                return;
            }

            if ( bytesRead > 0 )
            {
                connection.AssignedCallback.OnData( connection.ReceiveBuffer, bytesRead, connection.AssignedID );
            }
            else if( bytesRead == 0 )
            {
                Debug.Print( "Connection closed remotely" );
                Disconnect( connection );

                return;
            }

            try
            {
                connection.AssignedSocket.BeginReceive( connection.ReceiveBuffer, 0, connection.ReceiveBuffer.Count(), SocketFlags.None, new AsyncCallback( ReceiveCallback ), connection );
            }
            catch ( SocketException ex )
            {
                if ( ex.SocketErrorCode == SocketError.Shutdown || ex.SocketErrorCode == SocketError.ConnectionAborted || ex.SocketErrorCode == SocketError.ConnectionReset )
                {
                    Debug.WriteLine( "Connection {0} was closed.", connection.AssignedID );

                    Disconnect( connection );
                }
                else
                {
                    OnExceptionThrown( ex );
                }
            }
        }

        public event DisconnectedHandler Disconnected;
    }
}
